iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

系列:「從單一 agent 到多 agent 集群,再到接進我的生活」;不設天數,第 12 篇下半

上篇說完十六條紅怎麼分成四種、怎麼一種一種綠掉,留一條 route ratchet 沒修,等操作者裁
memory 的產品語意。這篇接著說:那條紅底下藏著另一件事——lib 從來沒有 Fresh 過;白天的
Gatekeeper 是什麼樣子;全量最後真的收工了,唯一一條紅正是留案的那條;還有一道差點連自己
都分不出放行跟繞過的閘。

build.rs 的兩種世界:修之前每一次 cargo 都重編 lib、重 link 全部 binary;修之後只有 HEAD 移動才重跑

F0:lib 一次都沒 Fresh 過

這一節是今天最大的一件事,而它不在早上的清單裡。

09:43:為什麼 targeted 在重編 lib

紅證 targeted 09:31 起跑,只有 5 支測試 binary,lib 原始碼自 414db8c4 起沒動。09:40 它的
cargo test --no-run 跑了 9 分鐘還在編;ps 看到 rustc 正在 --crate-name spectyn_mesh——
它在重編 lib。去讀 core/build.rs(8ef2455b 之前的版本,37 行):

    let git_dirty = run("git", &["status", "--porcelain"])           // :20-22
        .map(|s| if s.is_empty() { "" } else { "+" })
    println!("cargo:rustc-env=SPECTYN_GIT_HASH={}{}", git_hash, git_dirty);  // :31
    // Re-run when HEAD moves (covers commits + checkouts).
    println!("cargo:rerun-if-changed=.git/HEAD");                     // :35
    println!("cargo:rerun-if-changed=.git/index");                    // :36

lib 把 SPECTYN_GIT_HASH 烤進 core_sha()(core/src/lib.rs:216)。09:43 的假設是:它盯著
.git/index,而 git addgit status、tree 從 clean 翻到 dirty 都會改 index;每一次 index
一動,build.rs 重跑、env 變、lib 重編、202 支重 link、Gatekeeper 重付。這已經夠糟——而且看起來
解釋了今天早上為什麼要把 commit 集中一次做。派一個 worker 起草修法、一個駁斥者推翻。

紅證 Step 0:什麼都沒改,再 build 一次

worker 回來說:比那更糟。那兩條路徑是相對 core/,build script 的 cwd 是 core/,而
core/.git 不存在——repo 的 .git 在上一層。cargo 對一條找不到的 rerun-if-changed 路徑的
處理是:當它 stale。所以不是「index 一動就重編」,是每一次 cargo 都重編

先紅證,再套修法。09:57 tree 乾淨、HEAD 6ddf4ae4,CARGO_LOG=cargo::core::compiler::fingerprint=info,
cargo build -v 連跑兩次,中間不碰任何東西(f0-step0.log):

core/.git exists: NO                                                      (第 2 行)
fingerprint …/run-build-script-build-script-build.json: "paths":[".git/HEAD",".git/index"]  (第 3-5 行,三份)
=== BUILD 1 (09:57:04) ===
  stale: missing ".../spectyn-mesh-private/core/.git/HEAD"                (第 7 行)
  Dirty spectyn-mesh v0.6.0 (…/core): the file `.git/HEAD` is missing    (第 13 行)
  Running …/build-script-build                                            (第 15 行)
  Finished `dev` profile in 1m 05s                                        (第 16 行)
=== BUILD 2 (09:58:10) — NOTHING changed ===
  Dirty spectyn-mesh v0.6.0 (…/core): the file `.git/HEAD` is missing    (第 25 行)
  Running …/build-script-build                                            (第 27 行)
  Finished `dev` profile in 43.47s                                        (第 28 行)

BUILD 2 什麼都沒改:同一行 Dirty,build script 又跑,lib 又編了 43 秒。三份 fingerprint 檔
存的都是那兩條相對路徑。假設證實,而且是最糟的那個版本:這個 crate 的 lib 從這條 build.rs
進 tree 那天起,在這台機器上一次都沒有 Fresh 過。
修之後全量的 12m47s、紅證 targeted 的
16m46s、Day 8 到 Day 11 每一輪 Gatekeeper 的全額——每一輪都在重編一個沒有變的 lib,然後重
link、重審 202 支。

修法:只在 HEAD 移動時重跑

day12-f0-buildrs.diff,+76/−8,現在是 f3fcb84fcore/build.rs。三條規則,檔頭自己寫著
(build.rs:15-24):

  1. 不再盯 .git/index git addgit status(它會刷新 index)、每一次 clean↔dirty
    翻轉都會寫它(:16-17)。
  2. 路徑用 git rev-parse --git-path 解析(git_path(),:48-58)。git 自己會算這個
    worktree 的 git dir 在哪——主 tree 回 ../.git/HEAD,.wt/* 那種 linked worktree 回
    .git/worktrees/<name>/ 底下的絕對路徑(:18-23)。
  3. 只印出現在存在的檔(p.is_file().then_some(p),:57;規則 :24)。找不到的路徑,一條都不印。

盯的是四個檔(:88-108):HEADHEAD 指到的那個 ref 檔(ref: refs/heads/x
refs/heads/x)、packed-refslogs/HEAD。另外先印一條 rerun-if-changed=build.rs 當錨
(:67-71)——不然在沒有 git 的環境四個都 None,cargo 會退回「package 目錄任何東西變了就重跑」。

dirty 的 + 拿掉了(:85 現在只印 SPECTYN_GIT_HASH={git_hash})。rustc-env 是整個 package
共用的,給不了 bin 專用;而且不再隨 tree 改動重跑之後,編譯期的 dirty 旗標只會凍結在上次 HEAD
移動那一刻的狀態,會說謊(:26-29)。

綠證 Step 1:六次 build

修法套進 working tree、不 commit(探針要量 git add,commit 會動 HEAD),10:02:04 起
(f0-step1.log,HEAD 8ef2455b):

做了什麼 預期 結果 log 行
B1 build.rs 變了 Dirty 一次 Dirty … the file build.rs has changed,build script 跑,14m 50s 5-8
B2 什麼都沒改 Fresh Fresh spectyn-mesh v0.6.0,0.65 s 11-12
B3 touch 一個 docs 檔 + git add Fresh Fresh,0.30 s 16-17
B4 git status --porcelain Fresh Fresh,0.30 s 21-22
B5 core/src/lib.rs 加一行註解 Dirty,但 build script 不重跑 Dirty … src/lib.rs has changed,沒有 Running …/build-script-build 那行,45.49 s 26-28
B6 還原 lib.rs Dirty Dirty,36.31 s 32-34
spectyn --version 沒有 + spectyn 0.6.0 (8ef2455b46, macos-aarch64, built 2026-09-05)——tree 有 6 個檔髒著 38

B2 那個 Fresh 是這台機器上第一次。修之前同一個情境(Step 0 的 BUILD 2)是 Dirty + 43 秒。

B1 的 14 分 50 秒要解釋。build.rs 本身改了,lib 重編一次是預期的;但 B5、B6 同樣重編 lib 只要
36 到 45 秒。10:16 去看:rustc --crate-name spectyn_mesh 跑了 12 分鐘,累積 CPU 5.5 秒不再
增加,四條 thread 全 S;lsof 看到它載入了 21 個 proc-macro dylib;syspolicyd 65% CPU,
log show 說它在做 GK library assessment,每一筆去問 Apple 要公證票——ticket not availableError checking with notarization daemon: 3——5 分鐘 9 筆(live 看的,沒重量)。
Gatekeeper 換了個地方發作:不是執行剛 link 出來的 binary,是 rustc 載入剛編出來的 proc-macro
dylib,一顆一顆審、一顆一顆問 Apple。約 13 分鐘是它(估:14m50s 減掉 lib 本身 40 秒上下的重編)。
順帶一提:09:31 那輪紅證 targeted 編了 16m46s、lib 原始碼一個字沒動,多出的那 15 分鐘形狀跟
B1 一模一樣——那輪撞沒撞到這個 dylib 審核,沒去查,寫在這裡。

駁斥者補的三件

兩個駁斥者都沒推翻,各補了一件沒查到的:

  1. cargo 的 FAQ 本來就寫了這件事。「Why is Cargo rebuilding my code?」底下一條:
    「A build script prints cargo::rerun-if-changed=foo where foo is a file that doesn't exist
    and nothing generates it. In this case Cargo will keep running the build script thinking it will
    generate the file but nothing ever does. The fix is to avoid printing rerun-if-changed in this
    scenario.」抓自 doc.rust-lang.org/cargo/faq.html,一字不差。這條 build.rs 在文件寫明的坑裡
    待了多久,git log 可以查,今天沒查。
  2. .git/HEAD 是一個符號 ref,commit 不會改它。 它的內容是 ref: refs/heads/dev/hive-2026-08-14;
    commit 改的是那個 ref 檔。.git/HEAD 的 mtime 是 09-03 20:13:38,之後有 22 個 commit;
    refs/heads/dev/hive-2026-08-14logs/HEAD 的 mtime 是當天 14:35:37(最近一個 commit)。
    所以原本那行 rerun-if-changed=.git/HEAD 就算路徑對了,註解裡寫的「covers commits」也從來
    不成立——它只會在 checkout 時重跑。修法盯 ref 檔跟 logs/HEAD,就是為了這件事(build.rs:90-92
    :105-108)。
  3. cargo 重編依賴者的條件是 build script 的 run unit stale,跟它印出來的值有沒有變無關
    ——那個判斷在 script 跑之前就做了(build.rs:5-8)。所以「hash 沒變就不重編」這種期待從來
    不存在;唯一的修法是讓它不重跑。diff 的檔頭原本把機制寫成「output 檔 mtime 變了」,commit
    前改成駁斥者的版本。

這篇文章的前提要改

早上這輪工作全篇的前提是「lib 一改才重付」。前提錯了。不是「lib 一改」,是每一次。紅證
targeted lib 一個字沒動,照樣重編了 16 分鐘。

那排程還對嗎?對,但理由換了。Gatekeeper 收稅的單位是「新 link 出來、第一次被執行的 binary」:
5 支約 4 分鐘,202 支 11 小時。targeted 只 link 5 支,所以便宜——跟 lib 有沒有重編無關
lib 重編是另一筆稅(1 到 17 分鐘,今天量到的範圍),F0 修好之後這筆只在 HEAD 移動或 core/src
真的變了才付。

修好之後還有一筆:每一個 commit 都會動 ref 檔,build.rs 重跑,hash 變,lib 重編,202 支
重 link——包括只改 docs 的 commit。這是設計上的(hash 要對),不是 bug。10:24 的 lib commit
f3fcb84f 之後第一次 cargo(targeted)就付了這筆(targeted-fix.log:4 Compiling spectyn-mesh);
付完之後 14:31 的 Phase D 是 Finished 0.70s、14:34 的全量是 Finished 0.80s。對排程的意思是:
全量起跑之前把 commit 全部做完,build 階段不 commit;git addgit status、改 docs 檔,
現在都可以了(B3、B4)。

Gatekeeper 這盞燈

Gatekeeper 的兩張臉:nextest --list 逐支審 binary,rustc 載入 proc-macro dylib 也被審;出口是操作者才能按的開發者工具

Day 11 收工時寫:「修之後的全量在寫這一段的時候還在跑(20:13 起跑,201 支 binary 重 link,
Gatekeeper 80 分鐘)。」

實際量到的:

時間 狀態 證據
20:13:44 起跑,HEAD 414db8c4 full22-after.log:1
20:26 編完,12m 47s;之後行輸出 full22-after.log:275
06:27 20 個 --list 子行程在等,frontier p8_*,最老的等了 27:20 → 每支 ~80 s pgrep -P
06:31 20 支的窗只跨 4:07 → 每支 ~12 s;syspolicyd 0% 同上
07:39:34 --list 結束、測試開跑 junit-full-after-20260905-0836.xml timestamp
08:36:25 Summary:3593 / 3577 / 16 / 70,EXIT=100 full22-after.log:4297:4315

估 80 分鐘,實際光 --list 就跑了 11 小時 13 分。夜裡為什麼從 25 秒一支慢到 80 秒一支、
再回到 12 秒、再到 60 秒,原因沒查。

14:38,操作者回答了那個一直沒答的裁定點(D12-0):「加了,Terminal 已經在開發者工具裡。」但
full23 是 14:34:33、在加之前起跑的。14:40 量它:--list 子行程 etime 精確每 26 秒一支
(00:05、00:35、01:01…04:03),frontier cli_namespaces,syspolicyd 40.5%,log show 3 分鐘
16 筆 assessment——豁免對這棵已經在跑的行程樹沒有生效(估:它看的是新起的行程,不是已經
在跑的)。不殺、不重起;照 26 秒一支算,202 支估 87 分鐘,比夜裡的 11 小時好得多,但仍是一筆
稅。豁免是不是真的有效,要等 full23 收工後,另外起一輪新的 cargo 才量得到——這句話今天沒有
機會兌現,下面會說為什麼。

15:00 那一輪,批評者(CRIT)標了一個 blocker:這個 5 小時的 session 視窗,照今天已經死過兩次
的死法(每次死亡前約 25 到 26 個 subagent),估計會在全量的 Summary 落地之前見底——瓶頸是
session 用量,不是 CPU,cap=8 沒有對到這個限制。同一輪 CRIT 也把之前記錯的 binary 數量更正:
是 203 支、195 支沒被 --list 過,不是這份文件之前寫的 202/194(414db8c4 之後多了一支放行側
測試的 binary,一直沒被算進去)。

--list 段全程用 Monitor 盯著,不再逐輪 poll。16:00 前後 Monitor 觸發:Starting 3595 tests across 203 binaries (70 tests skipped)——203 支確認,CRIT 的更正成立。--list 段從 14:34:33
到這裡估 ~90 分鐘,跟「白天 26 秒/支 × 195 支未審過的」的估算一致。CRIT 擔心的視窗見底,這次
沒有發生——15:00 那輪之後每一輪都刻意派 0 個新 agent,把視窗留給等 Summary,撐到了 16:24
全量真正收工。

白天(daytime,已知 Terminal.app 早已在開發者工具裡的這一整段窗口)跟夜裡的對比,現在有兩個
數字可以放在一起:夜裡 11h13m、白天約 90 分鐘,快了大約 7 倍。但這不是豁免生效的證據——
豁免是 14:38 才按下去的,而 full23 這棵行程樹在那之前就起跑了,量到「豁免沒有立刻生效」的正是
今天。白天本來就比夜裡快(Day 11 量的白天基準是 ~25 秒一支,今天 26 秒一支,幾乎一樣),這筆
7 倍的差距目前只能歸給「白天,不是夜裡」,不能歸給「加了開發者工具豁免」。豁免對一次全新起跑的
--list 有沒有幫助,今天沒有測到——下一次全新的 cargo --list(而不是接續已經在跑的樹)才是
第一次真的量。

nextest 的 -E 篩哪一層

10:24 的 targeted 寫成 cargo nextest run -E 'binary(a) | binary(b) | … | (kind(lib) & test(…))'
(targeted-fix.log:2-3),以為它跟 --test a --test b 一樣只編那幾支。10:30 去看:六個 rustc
同時在編 p22_cli_gatefleet_live_smokeeval_insta_snapshot 這些跟 filter 無關的 test crate。
-E 的 filterset 只篩「跑哪些測試」,不篩 build——它把整個 package 的 test target 全編了,
Finished 在 15m23s。要縮 build 只能用 --test <name> / --lib

10:30 進一步寫「接著會對 202 支逐支 --list,Gatekeeper 全額」。這一半錯了。10:45 收工那行是
Starting 101 tests across 8 binaries (2730 tests and 195 binaries skipped)——binary()
篩 list,nextest 只對 8 支列舉,約 5 分鐘。所以 -E 篩兩層(執行、列舉)、不篩一層(build)。這
15 分鐘的稅沒有白付:它把 202 支 binary 都在 f3fcb84f 之後 link 好了,只是還沒過 Gatekeeper。
規則跟著來——targeted 之後、全量結束之前,不 commit;不然 HEAD 一動,lib 重編,202 支
重 link,剛付的稅作廢。

14:34:33 全量起跑(full23.log:1,HEAD c2a59c5b,dirty_repo=3——三個 docs 檔):
Finished … in 0.80s(第 281 行)。這個數字要說清楚是推論還是量到的:full23.log 從第 9 到 280
行全是編譯警告,grep -c Fresh 是 0——log 沒帶 -v,不印 Fresh。「202/203 支全部 Fresh、一支
都沒重編」是從 0.80 秒這個總時長推論出來的(比對 F0 綠證裡量到的 Fresh 就是零點幾秒這件事),
不是 log 白紙黑字寫的。

全量收工:第一份真的綠燈紀錄

16:24:02,Monitor 觸發,full23.log 印出:

START 2026-09-05 14:34:33 HEAD=c2a59c5b core_clean=1 dirty_core=0 dirty_repo=3   (第 1 行)
    Starting 3595 tests across 203 binaries (70 tests skipped)                   (第 284 行)
     Summary [1327.625s] 3595 tests run: 3594 passed (1 leaky), 1 failed, 70 skipped  (第 3899 行)
EXIT=100                                                                          (第 3902 行)
DURATION 6569s                                                                    (第 3906 行)
END 2026-09-05 16:24:02                                                           (第 3907 行)

14:34:33 到 16:24:02,DURATION 6569 秒 = 1 小時 49 分 29 秒:--list 約 90 分鐘 + 實跑
1327.625 秒(約 22 分鐘)。全程沒有殺過任何行程。

那一條紅,在第 3659 行(收工時又在第 3900 行重印一次——nextest 對失敗測試的慣例):

FAIL [   0.051s] (3375/3595) spectyn-mesh::the_app_only_calls_routes_that_exist every_daemon_path_the_app_builds_is_served
thread 'every_daemon_path_the_app_builds_is_served' (14721714) panicked at tests/the_app_only_calls_routes_that_exist.rs:170:5:
一條跟 hub_url 同行的字面路徑都沒有 —— 這條掃描已經對不上程式碼的形狀了

跟預測一模一樣——這就是上篇留下的那條 route ratchet:掃描器只認 "/…" 開頭的字面路徑,收不到
format!("{}/…", config.hub_url) 這種形狀,checked=0 撞自己的下限。它今天早就記在案、故意留紅,
等操作者裁定 memory.rs 三條路由的產品語意(D-memory)。1 leaky 沒有逐一追查,估計不影響本輪判定。

3594 passed、1 failed、70 skipped——不是零紅。而這正是這一天要交的東西。Day 11 收工時寫過兩句
話:「『跑了』跟『亮了』是兩件事」跟「正確的燈在沒人看的地方,等於壞的燈」
(day11-lights-that-never-went-red.md:443:447)。那九盞燈裡,三盞紅得對但沒人看,四盞從沒
亮過,綠燈紀錄那時候還不存在。今天這一條紅,反過來是那兩句話的另一面:它跑了、也真的亮了——
亮成紅色,而且亮的地方有人在看、寫進了這篇文章、附了裁定點等操作者接手。一條被看見、被記名、
等著被裁定的紅燈,跟一條沒人查的紅燈,不是同一件事;而能夠誠實地把這條紅跟另外 3594 條綠分
開寫清楚,前提正是上面兩節的稅跟閘——lib 真的 Fresh 過、全量真的跑到 Summary 那一行,而不是卡
--list 或被錯認成別的家族
。這是今天做出來的東西:不是「零紅」,是「知道剩幾條紅、為什麼
紅、紅得對不對、誰能裁」。

第三盞燈的續集:一道分不出放行跟繞過的閘

Day 11 把 paired_gate_check.sh 改嚴(c5948d9f):放行側要同時有簽章呼叫跟正向狀態斷言。
今天要把它接進 CI(Phase G)。第一次嘗試對 origin/main 跑,發現是飽和的——dev 分支 271 個
commit 一起看,總找得到一條放行斷言,第一次跑會綠不會紅——於是改成「受測變更的 base」(PR 用
base_ref、push 用 event.before)。然後量到比較難堪的事:

真放行也會紅。 scripts/paired_gate_check.sh:84OK_PAT 是單行 regex:
assert_eq!\([^;]*StatusCode::(OK|CREATED|…)。rustfmt 把帶長訊息的 assert_eq! 拆成三行,
assert_eq!(StatusCode::OK 不同行,它就看不見。對 c5948d9f..HEAD——含 Day 11 的閘修正
414db8c4 跟今天的紅證測試 2d062f40——跑它:exit 1,「新增的測試行裡沒有『等於成功狀態』
的斷言」。而那個範圍明明加了 the_app_callers_are_admitted.rs:103-106
skills_signed_caller_is_admitted.rs:165-171 兩條多行 assert_eq!(\n status,\n StatusCode::OK,
core/tests 裡這種多行放行斷言 9 處,單行 15 處。

意思是:base 一換窄,這道閘對「拆了閘沒補測試」會紅,對「補了測試而且是對的形狀」也會紅
一道分不出放行跟繞過的閘,不是閘。接上去的第一個紅,會是這次修法 commit 自己——一個誤報。

修法,跟它的三段證明

修法在腳本,不在 yaml:awk 從 assert_eq!( / assert!( 那一行起,把同一個 hunk 裡的續行
接上,接到含 ; 的那行為止;換檔或換 hunk 就中斷(續行不在這次 diff 裡的斷言不補,寧可漏報);
接完再剝一次字串——逐行剝的時候,跨行字串每一段只有一個引號,剝不掉,接成一行才是完整的
"…"assert_ne! 不接,它本來就不算放行側。

證明三段齊(g1-okpat-proof.txt),cwd、HEAD、範圍全部不變,只換腳本:

  • :原始腳本對 414db8c4~1(= c5948d9f)→ exit 1,「沒有『等於成功狀態』的斷言」。
  • :修過的腳本,同一個範圍 → exit 0,印出接回後正是那兩條 assert_eq!( status, StatusCode::OK, )
  • 仍紅:三個合成 commit。只拆閘、不補測試 → exit 1;補了測試但只有 assert_ne!(…401)
    且訊息字串裡故意寫了 StatusCode::OK → exit 1(接完再剝才擋得住這種騙法);補上真的多行
    assert_eq!(…, StatusCode::OK, …) → exit 0。同一段合成 commit 換回原始腳本 → 仍 exit 1:
    是 join 讓它綠的,不是別的。回歸:origin/main 仍 exit 0;40 個 0 仍「跳過」exit 0。

這輪之後,一個獨立的駁斥角色(G-1S)沒有推翻這個修法,但用 34 個合成案例攻擊它,量到兩點修正:
續行尾端的行內註解(// … StatusCode::OK)跟 tail-expr 沒有 ; 吞到下一個 fn 的兩種罕見情境,
會讓修過的版本誤放行。修正版收進 paired_gate_check.FIX.sh(也就是實際落地的版本),在
join 之前先剝掉每行尾端的 // 註解,擋掉這兩個新增的誤放。沒有全擋乾淨:格式引數裡的
StatusCode::OK(assert_eq!(status, UNAUTHORIZED, "want {}", StatusCode::OK))、多行版的
is_success()、跨行字串誤紅這幾個既有盲區還在——腳本自己的原則寫著「寧可漏報也不要吵到被
關掉」,這次先接受,寫進了駁斥者的報告裡,沒有藏起來。

兩份工作合起來,再對上 CI yaml 的組合方式——原本兩份 diff 同一個 hunk 各插一個 paired-gate:
job,硬併是重複 key;正確組合是 trigger 的 hunk + 新 job 的 hunk + 一支新腳本
scripts/ci/paired-gate-base.sh,由它算出「這次要審的那段改動」的 base:

pull_request → origin/<base_ref>       (PR 自己的 commit)
push         → github.event.before     (剛 push 上去的那幾個 commit)
               40 個 0(第一次 push)/ 抓不到(force-push 之後) → HEAD~1

event.before 是 40 個 0 時,paired_gate_check.sh 原本的 fallback 會把它當成一個真的
(不存在的)commit,git diff 000…0...HEAD fatal 之後被 || true 吞掉、diff 變空、「跳過」
exit 0——壞成綠的,不是判定;test_count_ratchet.sh 那邊更難看,set -euo pipefail 之下
git grep fatal,整支腳本直接以 exit 128 死掉,一行判定都沒印。paired-gate-base.sh
git rev-parse --verify '<base>^{commit}' 先驗成真的 commit 才交出去,解決這個問題。

一個 commit 98afb0d7(16:31):ci-fast.yml 的 push 加 dev/**(pull_request 仍然只認
[main]——PR 進 dev/** 時 origin/main...HEAD 是整條分支,前提本來就不成立);新 job
paired-gate 接進去,對 base-ref 版本跑,不是 origin/main;新增 paired-gate-base.sh
本機驗證(不需要 cargo):bash scripts/paired_gate_check.sh 414db8c4~1取代前的腳本
exit 1,對新腳本同一範圍 exit 0、印出接回後的 assert_eq!( status, StatusCode::OK, );YAML
用 loader 解析過、確認沒有重複 key、7 個 job、main-only 的 job 與 pr-serves-slug 零改動;
resolver 四種情境(push 40 個 0 → HEAD~1、pull_request base=main → origin/main 等)全部照
規格印出正確的 base。test_count_ratchet.sh 隨同接進同一個 paired-gate job。

沒有做的事,說清楚:GitHub 上的紅證——推一個拋棄式 dev/redproof-… 分支、看 paired-gate
job 真的在 GitHub 上紅過一次、然後刪掉分支——沒有做。
這不是猶豫,是這件事本身的性質:推分支
到遠端、在公開的 Actions 頁面留下紀錄、花掉 CI 的計費分鐘,這條線這個 session 不代跨。裁定點
D-phase-g-redproof 記在案,等操作者一句話。commit 訊息裡也是同一句:「先問操作者再做,不在
這個 commit 裡做。」

兩次撞上限

今天的協定是每 10 分鐘一輪、每輪 8 個 agent 開到上限。代價是 session 的用量上限撞了兩次,
兩次都是整段時間沒有任何工作被推進:

死掉的 回來的 cron
07:15–09:30 2h15m 4 個:文章草稿、paired gate base-ref、紀錄查核、Day 11 勘誤 5 個(兩份靜態編譯審閱、移植檢查、lib commit 組裝) 排了 13 次,0 次執行
10:45–14:30 3h45m 6 個:合併 CI diff、駁斥 OK_PAT、批評者、文章 v3、SVG、governance 文件掃描 2 個(多行 OK_PAT、文件一致性) 排了 20 次,0 次執行

六個小時、10 個 agent 死在半路。第一段剛好蓋住修之後全量收工(08:36)——它收工的時候沒有人在看。
第二段更難堪:10:45 targeted 101/101 綠、lib 修好了,然後三小時四十五分沒有人在。這一天的題目
是「讓那盞燈有人看」,而它綠的那一刻正好沒人。

死掉的 4 個第一次全部重派、都回來了;第二次死掉的 6 個 14:30 重派,全部收工。兩段的 build lock
狀態剛好相反——第一段全量在 --list,機器沒閒著;第二段 targeted 早收工了,機器空著,只是沒有人。

我在 Day 11 寫錯了四句,還有一句只說了一半

今天勘誤了 Day 11 的九處(5592be63,docs 版已套),歸成四句——三句是同一種錯:

  1. /sw.js 又把 HTML shell 放進 precache」——sw.js 從 167f96c7 起就沒有 /console,
    byte-identical。紅的是測試把兩行註解讀成 precache 項。
  2. 「時間斷言……量到的是這台 Mac」跟「機器,不是程式」——八次 panic 全在
    assert_eq!(status, 200),第一個請求就 401。這台從沒量到 p99;今天量到了,180 ms。
  3. 「16 條,五種」——16 條,四種;(a) 是 12 條。
  4. 「修好它會立刻點名 memory.rs 那三條」——重放是 8 條、3 個檔,只有 3 條等 memory 裁定;
    這句沒跑過,是估,下次寫「估」。

前三句是同一個錯:我讀了測試的名字跟 panic 訊息的第一行,沒讀 panic 的那一行在哪。 名字叫
latency,就寫機器;訊息說 precached,就寫 sw.js。

只說了一半的那句:「它跟程式碼無關,跟 binary 數量成正比,而且 lib 一改就全部重付」——前半對,
後半少了真正的原因:lib 不改也全部重付,因為 build.rs 每次都重跑。

Day 11 已經發表了

10:33 想把勘誤版重貼回 iThome 草稿頁,才發現:Day 11 已經發表(/articles/10407645
200,標題「Day 11:從來沒紅過的燈,不是綠燈」)。發表出去的那份沒有這九處勘誤。已發表的文章是
公開內容,不代改。修正版 body 備好(18,418 字元,勘誤 9 句 + 一處 bullet 修正),要不要把已發表
的 Day 11 更新成勘誤版、還是在 Day 12 文內勘誤就好,是操作者的裁定——D-day11-errata。這一節
就是文內勘誤的那一半;另一半等裁定。

這一天學到的

一、名字不是量測。 16 條紅分成五種,其中兩種是按測試名字分的:名字有 latency,就是機器;
訊息說 precached,就是 sw.js。今天讀 panic 行,兩種都不是。同一個 binary、同一個 401 body、
同一行——它是雙重驗章的第 12 條,從沒量到時間。一條紅要分到哪一種,看 panic 的那一行,不看
測試叫什麼。

二、下限要放在會少的那個粒度上。 「清點器只清點它名字裡那片」,第六次。這次特別難堪,因為
同一個測試檔裡另一條檢查已經學會了,deriver 就在幾百行下面,自己去讀了 serve.rs。而它有一條
總下限,看不出少一整個檔。修法不是往清單裡加三行,是讓 deriver 讀同一份 ROUTE_SOURCES,再給
每個檔一條下限:一個檔有 require_cluster_auth( 卻反推不出任何路由,就紅。

三、稅決定順序;但先量稅是什麼。 這台 Mac 對每一支新 binary 收 12 到 80 秒不等,202 支
一輪;所以會紅的測試先進 tree、lib 的補丁併一次 commit、build lock 被佔的時間拿來讀。順序是
對的。可是我以為 lib 重編是那筆稅的開關,F0 量出來它是另一筆稅,而且每一輪都在付。一個估錯八倍
的數字(80 分鐘 → 11 小時)跟一個沒量過的前提(lib 一改才重付)是同一種東西:寫「估」的還可以
勘誤,沒寫的連勘誤都沒得寫。

四、一道閘要分得出放行跟繞過。 對真實的 diff 跑改嚴後的 paired gate——這次修法自己——它
紅了,理由是找不到放行斷言,而放行斷言就在那裡,只是 rustfmt 把它折成三行。一道對真放行跟假
放行都說「不」的閘,跟一道都說「好」的閘,給的資訊一樣是零。這句話今天要再補一個限定:第一版
修法讓它對一段寫著 StatusCode::OK 的散文正確地說了一次「不」,但另一輪攻擊量到同一支腳本對
另外兩種「寫著 OK 的散文」(續行尾註解、跨 fn 吞行)仍然會說「好」——閘修好一次不等於閘修好,
要有人專門去攻擊它,才知道它擋得住哪些形狀、擋不住哪些。

五、一份誠實的紅,跟一份沒人看的紅,是兩件事。 Day 11 寫過:正確的燈在沒人看的地方,等於
壞的燈。今天全量收工,3594 綠、1 紅——那 1 紅不是意外,是 14:32 全量起跑前就寫下「預期紅:1
(route ratchet)」的那一條,跟預測一模一樣。這不是「終於全綠了」的故事;是「終於有一份紀錄,
能誠實地說清楚剩幾條紅、為什麼紅、誰能裁」的故事。零紅不是今天的目標,能不能把紅講清楚才是。

裁決點與下一步

今天收工時,四個裁定點在等操作者一句話,沒有一個是這個 session 能代裁的:

  • D-memory(承接 Day 10):三條 memory caller——刪 / 接 /rpc/recall 誠實改名 / 蓋後端。
    Phase E 的 route ratchet 卡在這裡:修好掃描器會點名 memory.rs 三條、agent.rs 兩條、
    networking.rs 三條,共 8 條、3 個檔;只有 memory.rs 那 3 條等這個裁定,另外 5 條(其中
    run_agent 那條又牽到下一個裁定點)要不要先清也是操作者的事。
  • D-second-daemon(承接):第二個 daemon 直接刪、還是先併再刪。今天不碰。
  • D-phase-g-redproof(新增):Phase G 的 config 已經本機驗證並落地(98afb0d7),但
    「paired gate 在 GitHub 上紅過一次」需要推一個拋棄式分支、觸發計費、看它真的紅、再刪掉分支
    ——這是推到遠端、公開可見、花 CI 分鐘的動作,等操作者一句話。
  • D-day11-errata:Day 11 已經發表,九句勘誤只進了 docs 版,要不要更新已發表的那份。

沒有裁定點的下一步比較直接:白天 Gatekeeper 的稅目前只量到「豁免對已起跑的行程樹沒有立刻生效」,
還沒有量到「豁免對一次全新起跑的 --list 有沒有幫助」——下一輪從乾淨狀態起跑的全量,會是第一次
真的測到它;Phase G 的 GitHub 紅證程序已經寫好、排練過本機版本,只等 D-phase-g-redproof 一句話
就能執行;而這篇文章本身,也是今天佇列裡跟裁定點無關、可以直接做完的最後一件事。


上一篇
Day 12(上):先證明它會紅
下一篇
Day 13:手機也是工作節點:先修正 Spectyn 的目標與驗收
系列文
從單一agent 到多agent 集群的開發流水帳以及應用18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言